/*-------------------------------------------------------------------------
 *
 * clog.c
 *		PostgreSQL 事务提交日志管理器
 *
 * 此模块替换了旧的“pg_log”访问代码，该代码将 pg_log 基本上视为一个关系，因为它通过常规缓冲管理器进行处理。这样做的问题在于，没有好的方法来回收存储空间，尤其是对于旧到永远不会再被查找的事务。现在，我们使用专用访问代码，以便将提交日志分解为相对较小、独立的部分。
 *
 * XLOG 交互：每当新 CLOG 页面初始化为零时，此模块会生成一个 XLOG 记录。CLOG 的其他写入来自于在 xact.c 中记录事务提交或中止，这会为这些事件生成自己的 XLOG 记录，并将重放状态更新；因此我们在这里无需额外的 XLOG 条目。对于同步事务提交，XLOG 确保在我们被调用以记录提交之前通过 XLOG 提交记录进行刷新，因此 WAL 规则“先写入 xlog 再写入数据”会自动满足。但是，对于异步提交，我们必须跟踪影响每个 CLOG 页面的最新 LSN，以便能够刷新 XLOG 到该位置并满足 WAL 规则。对于中止（无论是同步还是异步），我们无须担心，因为崩溃后的假设是这些事务无论如何都失败了。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/clog.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/clog.h"
#include "access/slru.h"
#include "access/transam.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
#include "miscadmin.h"
#include "pg_trace.h"
#include "pgstat.h"
#include "storage/proc.h"
#include "storage/sync.h"

/*
 * CLOG页面大小的定义。页面的大小与Postgres中其他地方使用的BLCKSZ相同。
 *
 * 注意：由于TransactionIds是32位并在0xFFFFFFFF处回绕，CLOG页面编号也在0xFFFFFFFF/CLOG_XACTS_PER_PAGE处回绕，而CLOG段编号在0xFFFFFFFF/CLOG_XACTS_PER_PAGE/SLRU_PAGES_PER_SEGMENT处回绕。我们在此模块中无需明确考虑这一事实，除了在TruncateCLOG中比较段和页编号时（见CLOGPagePrecedes）。
 */

/* 我们每个事务需要两个比特，因此四个事务放在一个字节中 */
#define CLOG_BITS_PER_XACT	2
#define CLOG_XACTS_PER_BYTE 4
#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE)
#define CLOG_XACT_BITMASK	((1 << CLOG_BITS_PER_XACT) - 1)

#define TransactionIdToPage(xid)	((xid) / (TransactionId) CLOG_XACTS_PER_PAGE)
#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE)
#define TransactionIdToByte(xid)	(TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE)
#define TransactionIdToBIndex(xid)	((xid) % (TransactionId) CLOG_XACTS_PER_BYTE)

/* 我们为每组事务存储最新的异步LSN */
#define CLOG_XACTS_PER_LSN_GROUP	32	/* 保持这是2的幂 */
#define CLOG_LSNS_PER_PAGE	(CLOG_XACTS_PER_PAGE / CLOG_XACTS_PER_LSN_GROUP)

#define GetLSNIndex(slotno, xid)	((slotno) * CLOG_LSNS_PER_PAGE + \
	((xid) % (TransactionId) CLOG_XACTS_PER_PAGE) / CLOG_XACTS_PER_LSN_GROUP)

/*
 * 我们认为低于此数量的子事务开始应用clog组更新优化。测试表明，数量高于此值可能会损害性能。
 */
#define THRESHOLD_SUBTRANS_CLOG_OPT	5

/*
 * 连接到用于CLOG控制的共享内存数据结构
 */
static SlruCtlData XactCtlData;

#define XactCtl (&XactCtlData)


static int	fc_ZeroCLOGPage(int fc_pageno, bool fc_writeXlog);
static bool fc_CLOGPagePrecedes(int fc_page1, int fc_page2);
static void fc_WriteZeroPageXlogRec(int fc_pageno);
static void fc_WriteTruncateXlogRec(int fc_pageno, TransactionId fc_oldestXact,
								 Oid fc_oldestXactDb);
static void fc_TransactionIdSetPageStatus(TransactionId fc_xid, int fc_nsubxids,
									   TransactionId *fc_subxids, XidStatus fc_status,
									   XLogRecPtr fc_lsn, int fc_pageno,
									   bool fc_all_xact_same_page);
static void fc_TransactionIdSetStatusBit(TransactionId fc_xid, XidStatus fc_status,
									  XLogRecPtr fc_lsn, int fc_slotno);
static void fc_set_status_by_pages(int fc_nsubxids, TransactionId *fc_subxids,
								XidStatus fc_status, XLogRecPtr fc_lsn);
static bool fc_TransactionGroupUpdateXidStatus(TransactionId fc_xid,
											XidStatus fc_status, XLogRecPtr fc_lsn, int fc_pageno);
static void fc_TransactionIdSetPageStatusInternal(TransactionId fc_xid, int fc_nsubxids,
											   TransactionId *fc_subxids, XidStatus fc_status,
											   XLogRecPtr fc_lsn, int fc_pageno);


/*
 * TransactionIdSetTreeStatus
 *
 * 记录事务及其子事务树在提交日志中的最终状态。要确保这一过程
 * 高效且尽可能原子化。
 *
 * xid 是一个单一的 xid，用于设置状态。这通常是
 * 顶层提交或回滚的顶层 transactionid。它也可以
 * 是记录事务回滚时的子事务。
 *
 * subxids 是一个长度为 nsubxids 的 xid 数组，表示树中
 * 的子事务。在某些情况下 nsubxids 可能为零。
 *
 * lsn 必须是在记录异步提交时的提交记录的 WAL 位置。
 * 对于同步提交，它可以是 InvalidXLogRecPtr，因为
 * 调用者保证在这种情况下提交记录已经被刷新。它
 * 在回滚情况下也应该是 InvalidXLogRecPtr。
 *
 * 在提交的情况下，原子性受到所有 subxids 是否与
 * xid 在同一 CLOG 页的限制。如果它们都在，那么锁将
 * 仅被获取一次，并且状态将被直接设置为已提交。
 * 否则我们必须
 *   1. 将所有不在与主 xid 同一页面上的 subxids 设置为
 *      子已提交
 *   2. 原子性地将主 xid 和同一页面上的 subxids 设置为已提交
 *   3. 再次遍历第一批，将它们设置为已提交
 * 请注意，就并发检查者而言，主事务
 * 整体的提交仍然是原子的。
 *
 * 示例：
 *      TransactionId t 提交，并具有子 xid t1, t2, t3, t4
 *      t 在页面 p1，t1 也在 p1，t2 和 t3 在 p2，t4 在 p3
 *      1. 更新页面2-3：
 *              page2: 将 t2,t3 设置为子已提交
 *              page3: 将 t4 设置为子已提交
 *      2. 更新 page1:
 *              将 t1 设置为子已提交，
 *              然后将 t 设置为已提交，
 *              然后将 t1 设置为已提交
 *      3. 更新页面2-3：
 *              page2: 将 t2,t3 设置为已提交
 *              page3: 将 t4 设置为已提交
 *
 * 注意：这是一个低级例程，并不是大多数使用情境的首选入口点；
 * transam.c 中的函数是预期的调用者。
 *
 * XXX 考虑对我们将需要但尚未在缓存中的页面发出 POSIX_FADV_WILLNEED，
 * 以及提示页面不再从缓存中消失。
 */
void TransactionIdSetTreeStatus(TransactionId fc_xid, int fc_nsubxids,
						   TransactionId *fc_subxids, XidStatus fc_status, XLogRecPtr fc_lsn)
{
	int			fc_pageno = TransactionIdToPage(fc_xid);	/* 获取父节点的页面 */
	int			fc_i;

	Assert(fc_status == TRANSACTION_STATUS_COMMITTED ||
		   fc_status == TRANSACTION_STATUS_ABORTED);

	/*
	 * 查看是否有多少 subxids 位于与父节点相同的页面上（如果有的话）。
	 */
	for (fc_i = 0; fc_i < fc_nsubxids; fc_i++)
	{
		if (TransactionIdToPage(fc_subxids[fc_i]) != fc_pageno)
			break;
	}

	/*
	 * 所有项目是否都适合单个页面？
	 */
	if (fc_i == fc_nsubxids)
	{
		/*
		 * 在单个调用中设置父节点和所有子事务
		 */
		fc_TransactionIdSetPageStatus(fc_xid, fc_nsubxids, fc_subxids, fc_status, fc_lsn,
								   fc_pageno, true);
	}
	else
	{
		int			fc_nsubxids_on_first_page = fc_i;

		/*
		 * 如果这是一次提交，那么我们关心正确地执行此操作（即，
		 * 使用子已提交的中间状态）。到这里为止，我们知道
		 * 我们正在更新多个 clog 页，因此我们必须标记
		 * 未在第一页上的条目，以便它们在我们返回更新状态为
		 * 完全已提交之前显示为子已提交。
		 *
		 * 为避免两次访问第一页，跳过标记该第一页上的
		 * subxids 为子已提交。
		 */
		if (fc_status == TRANSACTION_STATUS_COMMITTED)
			fc_set_status_by_pages(fc_nsubxids - fc_nsubxids_on_first_page,
								fc_subxids + fc_nsubxids_on_first_page,
								TRANSACTION_STATUS_SUB_COMMITTED, fc_lsn);

		/*
		 * 现在将父事务和子事务设置在同一页上（如果有的话）
		 */
		fc_pageno = TransactionIdToPage(fc_xid);
		fc_TransactionIdSetPageStatus(fc_xid, fc_nsubxids_on_first_page, fc_subxids, fc_status,
								   fc_lsn, fc_pageno, false);

		/*
		 * 现在以单个 clog 页为单位处理其余的 subxids，从第二页开始，
		 * 就像我们之前做的那样。
		 */
		fc_set_status_by_pages(fc_nsubxids - fc_nsubxids_on_first_page,
							fc_subxids + fc_nsubxids_on_first_page,
							fc_status, fc_lsn);
	}
}

/*
 * TransactionIdSetTreeStatus 的辅助功能：为一组
 * 事务设置状态，涉及到单独的 CLOG 页面。我们从不
 * 将整个事务树传递给此函数，仅传递与顶级事务 ID
 * 在不同页面上的子事务。
 */
static void fc_set_status_by_pages(int fc_nsubxids, TransactionId *fc_subxids,
					XidStatus fc_status, XLogRecPtr fc_lsn)
{
	int			fc_pageno = TransactionIdToPage(fc_subxids[0]);
	int			fc_offset = 0;
	int			fc_i = 0;

	Assert(fc_nsubxids > 0);		/* 否则上面的 pageno 获取是不安全的 */

	while (fc_i < fc_nsubxids)
	{
		int			fc_num_on_page = 0;
		int			fc_nextpageno;

		do
		{
			fc_nextpageno = TransactionIdToPage(fc_subxids[fc_i]);
			if (fc_nextpageno != fc_pageno)
				break;
			fc_num_on_page++;
			fc_i++;
		} while (fc_i < fc_nsubxids);

		fc_TransactionIdSetPageStatus(InvalidTransactionId,
								   fc_num_on_page, fc_subxids + fc_offset,
								   fc_status, fc_lsn, fc_pageno, false);
		fc_offset = fc_i;
		fc_pageno = fc_nextpageno;
	}
}

/*
 * 记录单个页面上所有事务条目的最终状态到提交日志中。 
 * 仅在此页面上原子。
 */
static void fc_TransactionIdSetPageStatus(TransactionId fc_xid, int fc_nsubxids,
						   TransactionId *fc_subxids, XidStatus fc_status,
						   XLogRecPtr fc_lsn, int fc_pageno,
						   bool fc_all_xact_same_page)
{
	/* 当 PGPROC 溢出时，无法使用组更新。 */
	StaticAssertStmt(THRESHOLD_SUBTRANS_CLOG_OPT <= PGPROC_MAX_CACHED_SUBXIDS,
					 "group clog threshold less than PGPROC cached subxids");

	/*
	 * 当在 XactSLRULock 上发生争用时，我们尝试将多个
	 * 更新分组；单个领导进程将为多个后端执行事务状态
	 * 更新，从而减少需要获取 XactSLRULock 的次数。
	 *
	 * 为了使此优化安全，MyProc 中的 XID 和 subxids 必须
	 * 与我们要设置状态的 XID 相同。检查这一点是否成立。
	 *
	 * 为了使此优化高效，我们不应有太多
	 * 子 XIDs，并且我们正在调整 clog 的所有 XIDs 应该
	 * 在同一页面上。也检查这些条件。
	 */
	if (fc_all_xact_same_page && fc_xid == MyProc->xid &&
		fc_nsubxids <= THRESHOLD_SUBTRANS_CLOG_OPT &&
		fc_nsubxids == MyProc->subxidStatus.count &&
		(fc_nsubxids == 0 ||
		 memcmp(fc_subxids, MyProc->subxids.xids,
				fc_nsubxids * sizeof(TransactionId)) == 0))
	{
		/*
		 * 如果我们可以立即获取 XactSLRULock，我们更新
		 * 自己的 XID 状态并释放锁。如果不能，尝试使用
		 * 组 XID 更新。如果那样做不奏效，退回等待
		 * 锁来执行仅此事务的更新。
		 */
		if (LWLockConditionalAcquire(XactSLRULock, LW_EXCLUSIVE))
		{
			/* 在未等待的情况下获得锁！进行更新。 */
			fc_TransactionIdSetPageStatusInternal(fc_xid, fc_nsubxids, fc_subxids, fc_status,
											   fc_lsn, fc_pageno);
			LWLockRelease(XactSLRULock);
			return;
		}
		else if (fc_TransactionGroupUpdateXidStatus(fc_xid, fc_status, fc_lsn, fc_pageno))
		{
			/* 组更新机制已经完成工作。 */
			return;
		}

		/* 仅在更新尚未完成时继续。 */
	}

	/* 组更新不适用，或无法接受此页面号。 */
	LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);
	fc_TransactionIdSetPageStatusInternal(fc_xid, fc_nsubxids, fc_subxids, fc_status,
									   fc_lsn, fc_pageno);
	LWLockRelease(XactSLRULock);
}

/*
 * 记录事务条目在提交日志中的最终状态
 *
 * 这里不做任何锁定；调用者必须处理这个。
 */
static void fc_TransactionIdSetPageStatusInternal(TransactionId fc_xid, int fc_nsubxids,
								   TransactionId *fc_subxids, XidStatus fc_status,
								   XLogRecPtr fc_lsn, int fc_pageno)
{
	int			fc_slotno;
	int			fc_i;

	Assert(fc_status == TRANSACTION_STATUS_COMMITTED ||
		   fc_status == TRANSACTION_STATUS_ABORTED ||
		   (fc_status == TRANSACTION_STATUS_SUB_COMMITTED && !TransactionIdIsValid(fc_xid)));
	Assert(LWLockHeldByMeInMode(XactSLRULock, LW_EXCLUSIVE));

	/*
	 * 如果我们正在进行异步提交（即，lsn 有效），那么我们必须等待
	 * 页面槽上的任何活动写入完成。否则我们的
	 * 更新可能会在该写入中到达磁盘，而这将是不可行的，因为
	 * 我们必须在完成适当的 WAL 刷新之前，不让它到达磁盘。
	 * 但是当 lsn 无效时，稍微在一个被写入繁忙的页面上随意操作是可以的，
	 * 因为我们不在乎更新是否比我们想象的更早到达磁盘。
	 */
	fc_slotno = SimpleLruReadPage(XactCtl, fc_pageno, XLogRecPtrIsInvalid(fc_lsn), fc_xid);

	/*
	 * 设置主事务 ID（如果有的话）。
	 *
	 * 如果我们在这个页面上更新多个 xid，而它正在被写入，
	 * 我们可能会发现一些位写入了磁盘，其他的没有。
	 * 如果我们正在更新具有顶级 xid 的页面上的提交，这可能会破坏原子性，
	 * 因此我们在标记顶级提交之前先对子 xid 进行子提交。
	 */
	if (TransactionIdIsValid(fc_xid))
	{
		/* 如果需要，先处理子事务... */
		if (fc_status == TRANSACTION_STATUS_COMMITTED)
		{
			for (fc_i = 0; fc_i < fc_nsubxids; fc_i++)
			{
				Assert(XactCtl->shared->page_number[fc_slotno] == TransactionIdToPage(fc_subxids[fc_i]));
				fc_TransactionIdSetStatusBit(fc_subxids[fc_i],
										  TRANSACTION_STATUS_SUB_COMMITTED,
										  fc_lsn, fc_slotno);
			}
		}

		/* ... 然后是主要事务 */
		fc_TransactionIdSetStatusBit(fc_xid, fc_status, fc_lsn, fc_slotno);
	}

	/* 设置子事务 */
	for (fc_i = 0; fc_i < fc_nsubxids; fc_i++)
	{
		Assert(XactCtl->shared->page_number[fc_slotno] == TransactionIdToPage(fc_subxids[fc_i]));
		fc_TransactionIdSetStatusBit(fc_subxids[fc_i], fc_status, fc_lsn, fc_slotno);
	}

	XactCtl->shared->page_dirty[fc_slotno] = true;
}

/*
 * 当我们无法在提交时立即以独占模式获取 XactSLRULock 时，
 * 将自己添加到需要更新 XIDs 状态的进程列表中。
 * 第一个将自己添加到列表的进程将以独占模式获取 
 * XactSLRULock，并代表所有组成员设置事务状态。
 * 这避免了在多个进程尝试同时提交时 
 * XactSLRULock 的大量竞争，因为锁不需要
 * 从一个提交进程重复传递到下一个。
 *
 * 当事务状态已在 clog 中更新时返回 true；如果我们决定不应用优化，因为我们需要更新的页面
 * 编号与已在等待的进程不同，则返回 false。
 */
static bool fc_TransactionGroupUpdateXidStatus(TransactionId fc_xid, XidStatus fc_status,
								XLogRecPtr fc_lsn, int fc_pageno)
{
	volatile PROC_HDR *fc_procglobal = ProcGlobal;
	PGPROC	   *fc_proc = MyProc;
	uint32		fc_nextidx;
	uint32		fc_wakeidx;

	/* 我们肯定应该有一个需要更新状态的 XID。 */
	Assert(TransactionIdIsValid(fc_xid));

	/*
	 * 将自己添加到需要组 XID 状态更新的进程列表中。
	 */
	fc_proc->clogGroupMember = true;
	fc_proc->clogGroupMemberXid = fc_xid;
	fc_proc->clogGroupMemberXidStatus = fc_status;
	fc_proc->clogGroupMemberPage = fc_pageno;
	fc_proc->clogGroupMemberLsn = fc_lsn;

	fc_nextidx = pg_atomic_read_u32(&fc_procglobal->clogGroupFirst);

	while (true)
	{
		/*
		 * 如果我们需要更新的当前事务状态的 clog 页面与
		 * 组领导者的 clog 页面相同，则将 proc 添加到列表中。
		 *
		 * 在这里有一个竞争条件，即在执行以下检查后
		 * 并在将此 proc 的 clog 更新添加到组之前，
		 * 组领导者可能已经完成了该页面的组更新并成为
		 * 另一个组的组领导者。这将导致一个
		 * 单个组可能有不同的 clog 页面
		 * 更新。这不太可能发生并且仍然有效，
		 * 只是可能效率稍低一些。
		 */
		if (fc_nextidx != INVALID_PGPROCNO &&
			ProcGlobal->allProcs[fc_nextidx].clogGroupMemberPage != fc_proc->clogGroupMemberPage)
		{
			/*
			 * 确保此 proc 不是任何需要 XID 状态更新的 clog 组的成员。
			 */
			fc_proc->clogGroupMember = false;
			pg_atomic_write_u32(&fc_proc->clogGroupNext, INVALID_PGPROCNO);
			return false;
		}

		pg_atomic_write_u32(&fc_proc->clogGroupNext, fc_nextidx);

		if (pg_atomic_compare_exchange_u32(&fc_procglobal->clogGroupFirst,
										   &fc_nextidx,
										   (uint32) fc_proc->pgprocno))
			break;
	}

	/*
	 * 如果列表不为空，领导者将更新我们的 XID 状态。
	 * 不可能在没有领导者的情况下有追随者，因为
	 * 首先添加到列表中的进程将始终具有
	 * nextidx 作为 INVALID_PGPROCNO。
	 */
	if (fc_nextidx != INVALID_PGPROCNO)
	{
		int			fc_extraWaits = 0;

		/* 睡眠直到领导者更新我们的 XID 状态。 */
		pgstat_report_wait_start(WAIT_EVENT_XACT_GROUP_UPDATE);
		for (;;)
		{
			/* 充当读取屏障 */
			PGSemaphoreLock(fc_proc->sem);
			if (!fc_proc->clogGroupMember)
				break;
			fc_extraWaits++;
		}
		pgstat_report_wait_end();

		Assert(pg_atomic_read_u32(&fc_proc->clogGroupNext) == INVALID_PGPROCNO);

		/* 修复被吸收的唤醒的信号量计数 */
		while (fc_extraWaits-- > 0)
			PGSemaphoreUnlock(fc_proc->sem);
		return true;
	}

	/* 我们是领导者。 代表所有人获取锁。 */
	LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);

	/*
	 * 现在我们已经获得了锁，清空等待
	 * 组 XID 状态更新的进程列表，保存列表头的指针。
	 * 一次弹出一个元素可能会导致 ABA 问题。
	 */
	fc_nextidx = pg_atomic_exchange_u32(&fc_procglobal->clogGroupFirst,
									 INVALID_PGPROCNO);

	/* 记住列表的头，以便在释放锁后进行唤醒。 */
	fc_wakeidx = fc_nextidx;

	/* 遍历列表并更新所有 XIDs 的状态。 */
	while (fc_nextidx != INVALID_PGPROCNO)
	{
		PGPROC	   *fc_proc = &ProcGlobal->allProcs[fc_nextidx];

		/*
		 * 超过 THRESHOLD_SUBTRANS_CLOG_OPT 子 XIDs 的事务
		 * 不应使用组 XID 状态更新机制。
		 */
		Assert(fc_proc->subxidStatus.count <= THRESHOLD_SUBTRANS_CLOG_OPT);

		fc_TransactionIdSetPageStatusInternal(fc_proc->clogGroupMemberXid,
										   fc_proc->subxidStatus.count,
										   fc_proc->subxids.xids,
										   fc_proc->clogGroupMemberXidStatus,
										   fc_proc->clogGroupMemberLsn,
										   fc_proc->clogGroupMemberPage);

		/* 移动到列表中的下一个 proc。 */
		fc_nextidx = pg_atomic_read_u32(&fc_proc->clogGroupNext);
	}

	/* 我们现在已经完成了锁。 */
	LWLockRelease(XactSLRULock);

	/*
	 * 现在我们已经释放了锁，回去唤醒每个人。
	 * 我们在锁下不这样做，以保持锁持有时间尽可能短。
	 */
	while (fc_wakeidx != INVALID_PGPROCNO)
	{
		PGPROC	   *fc_proc = &ProcGlobal->allProcs[fc_wakeidx];

		fc_wakeidx = pg_atomic_read_u32(&fc_proc->clogGroupNext);
		pg_atomic_write_u32(&fc_proc->clogGroupNext, INVALID_PGPROCNO);

		/* 确保所有先前的写入在跟随者继续之前都是可见的。 */
		pg_write_barrier();

		fc_proc->clogGroupMember = false;

		if (fc_proc != MyProc)
			PGSemaphoreUnlock(fc_proc->sem);
	}

	return true;
}

/*
 * 设置单个事务的提交状态。
 *
 * 必须在持有 XactSLRULock 的情况下调用
 */
static void fc_TransactionIdSetStatusBit(TransactionId fc_xid, XidStatus fc_status, XLogRecPtr fc_lsn, int fc_slotno)
{
	int			fc_byteno = TransactionIdToByte(fc_xid);
	int			fc_bshift = TransactionIdToBIndex(fc_xid) * CLOG_BITS_PER_XACT;
	char	   *fc_byteptr;
	char		fc_byteval;
	char		fc_curval;

	fc_byteptr = XactCtl->shared->page_buffer[fc_slotno] + fc_byteno;
	fc_curval = (*fc_byteptr >> fc_bshift) & CLOG_XACT_BITMASK;

	/*
	 * 在恢复期间重放事务时，我们仍然需要执行
	 * 子提交和提交的两个阶段。然而，一些事务
	 * 已经正确标记，因此我们只需将这些视为无操作，
	 * 这使我们可以尽可能严格地保持以下的 Assert。
	 */
	if (InRecovery && fc_status == TRANSACTION_STATUS_SUB_COMMITTED &&
		fc_curval == TRANSACTION_STATUS_COMMITTED)
		return;

	/*
	 * 当前状态变化应为从 0 或子提交到目标状态，
	 * 或者在恢复期间重放更改时我们应该已经到达那里。
	 */
	Assert(fc_curval == 0 ||
		   (fc_curval == TRANSACTION_STATUS_SUB_COMMITTED &&
			fc_status != TRANSACTION_STATUS_IN_PROGRESS) ||
		   fc_curval == fc_status);

	/* 请注意，这假设对 clog 页面有独占访问 */
	fc_byteval = *fc_byteptr;
	fc_byteval &= ~(((1 << CLOG_BITS_PER_XACT) - 1) << fc_bshift);
	fc_byteval |= (fc_status << fc_bshift);
	*fc_byteptr = fc_byteval;

	/*
	 * 如果事务完成的 LSN 更高，则更新组 LSN。
	 *
	 * 注意：在 InRecovery 处理期间提供时，lsn 将无效，
	 * 因此，我们不需要采取任何特别措施来避免在
	 * 恢复期间更新 LSN。恢复完成后，下一个 clog 变更将
	 * 正确设置 LSN。
	 */
	if (!XLogRecPtrIsInvalid(fc_lsn))
	{
		int			fc_lsnindex = GetLSNIndex(fc_slotno, fc_xid);

		if (XactCtl->shared->group_lsn[fc_lsnindex] < fc_lsn)
			XactCtl->shared->group_lsn[fc_lsnindex] = fc_lsn;
	}
}

/*
 * 在提交日志中查询事务的状态。
 *
 * 除了实际的提交状态外，此函数会返回（放入 *lsn）
 * 一个 LSN，该 LSN 足够晚，以能够保证如果我们刷新到
 * 该 LSN，那么我们将事务的提交记录刷写到磁盘。
 * 结果不一定是事务提交记录的确切 LSN！例如，对于早已
 * 过去的事务（那些其 clog 页面已迁移到磁盘的事务），
 * 我们将返回 InvalidXLogRecPtr。此外，由于我们在同一
 * clog 页面上对事务进行分组以节省存储，因此我们可能
 * 会返回属于同一组的后续事务的 LSN。
 *
 * 注意：这是一个低级例程，并不是大多数使用的首选入口点；
 * transam.c 中的 TransactionLogFetch() 是预期的调用方。
 */
XidStatus
TransactionIdGetStatus(TransactionId fc_xid, XLogRecPtr *fc_lsn)
{
	int			fc_pageno = TransactionIdToPage(fc_xid);
	int			fc_byteno = TransactionIdToByte(fc_xid);
	int			fc_bshift = TransactionIdToBIndex(fc_xid) * CLOG_BITS_PER_XACT;
	int			fc_slotno;
	int			fc_lsnindex;
	char	   *fc_byteptr;
	XidStatus	fc_status;

	/* 锁由 SimpleLruReadPage_ReadOnly 获取 */

	fc_slotno = SimpleLruReadPage_ReadOnly(XactCtl, fc_pageno, fc_xid);
	fc_byteptr = XactCtl->shared->page_buffer[fc_slotno] + fc_byteno;

	fc_status = (*fc_byteptr >> fc_bshift) & CLOG_XACT_BITMASK;

	fc_lsnindex = GetLSNIndex(fc_slotno, fc_xid);
	*fc_lsn = XactCtl->shared->group_lsn[fc_lsnindex];

	LWLockRelease(XactSLRULock);

	return fc_status;
}

/*
 * 共享 CLOG 缓冲区的数量。
 *
 * 在较大的多处理器系统上，可能会同时有许多 CLOG 页面
 * 请求，这可能会导致对 CLOG 页面的磁盘访问，如果所需
 * 页面未在内存中找到。测试表明，我们通过拥有 128 个
 * CLOG 缓冲区可以获得最佳性能，超过这个数量不会提高
 * 性能。
 *
 * 无条件将 CLOG 缓冲区数量保持在 128 似乎并不是一个
 * 好主意，因为这会增加启动所需的最小共享内存量，
 * 这对运行非常小配置的人来说可能是个问题。下面的
 * 公式似乎代表了一个合理的折中：共享缓冲区值非常低的人
 * 将获得更少的 CLOG 缓冲区，而其他人将获得 128。
 */
Size CLOGShmemBuffers(void)
{
	return Min(128, Max(4, NBuffers / 512));
}

/*
 * CLOG 的共享内存初始化
 */
Size CLOGShmemSize(void)
{
	return SimpleLruShmemSize(CLOGShmemBuffers(), CLOG_LSNS_PER_PAGE);
}

void CLOGShmemInit(void)
{
	XactCtl->PagePrecedes = fc_CLOGPagePrecedes;
	SimpleLruInit(XactCtl, "Xact", CLOGShmemBuffers(), CLOG_LSNS_PER_PAGE,
				  XactSLRULock, "pg_xact", LWTRANCHE_XACT_BUFFER,
				  SYNC_HANDLER_CLOG);
	SlruPagePrecedesUnitTests(XactCtl, CLOG_XACTS_PER_PAGE);
}

/*
 * 此函数必须在系统安装时调用一次。它创建
 * 初始的 CLOG 段。（CLOG 目录假定已由 initdb 创建，
 * 并且必须已经调用了 CLOGShmemInit。）
 */
void BootStrapCLOG(void)
{
	int			fc_slotno;

	LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);

	/* 创建并将提交日志的第一页置零 */
	fc_slotno = fc_ZeroCLOGPage(0, false);

	/* 确保它被写出 */
	SimpleLruWritePage(XactCtl, fc_slotno);
	Assert(!XactCtl->shared->page_dirty[fc_slotno]);

	LWLockRelease(XactSLRULock);
}

/*
 * 初始化（或重新初始化）CLOG 的一页为零。
 * 如果 writeXlog 为真，还会发出一个 XLOG 记录，说明我们这样做了。
 *
 * 页面实际上并未被写入，只是在共享内存中设置。
 * 返回新页面的槽号。
 *
 * 入口时必须持有控制锁，并在退出时保持该锁。
 */
static int fc_ZeroCLOGPage(int fc_pageno, bool fc_writeXlog)
{
	int			fc_slotno;

	fc_slotno = SimpleLruZeroPage(XactCtl, fc_pageno);

	if (fc_writeXlog)
		fc_WriteZeroPageXlogRec(fc_pageno);

	return fc_slotno;
}

/*
 * 在 postmaster 或独立后端启动期间，必须在 StartupXLOG 初始化
 * ShmemVariableCache->nextXid 后调用一次。
 */
void StartupCLOG(void)
{
	TransactionId fc_xid = XidFromFullTransactionId(ShmemVariableCache->nextXid);
	int			fc_pageno = TransactionIdToPage(fc_xid);

	LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);

	/*
	 * 初始化我们对最新页码的概念。
	 */
	XactCtl->shared->latest_page_number = fc_pageno;

	LWLockRelease(XactSLRULock);
}

/*
 * 在启动/恢复结束时，必须调用一次此函数。
 */
void TrimCLOG(void)
{
	TransactionId fc_xid = XidFromFullTransactionId(ShmemVariableCache->nextXid);
	int			fc_pageno = TransactionIdToPage(fc_xid);

	LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);

	/*
	 * 将当前 CLOG 页的其余部分置零。在正常情况下，
	 * 它应该已经是零，但至少理论上有可能，XLOG 回放
	 * 将确定一个小于上一个实际使用的 XID 的 nextXID
	 * 值，并且由先前的数据库生命周期标记（因为子事务提交
	 * 写入 CLOG 但不生成 WAL 条目）。让我们只为安全起见。
	 * （我们不必担心当前页面之外的页面，因为这些页面在首次
	 * 使用时会被置零。出于同样的原因，当 nextXid 恰好处于页面
	 * 边界时也无需做任何事情；而且在这种情况下“当前”页面可能
	 * 尚不存在。）
	 */
	if (TransactionIdToPgIndex(fc_xid) != 0)
	{
		int			fc_byteno = TransactionIdToByte(fc_xid);
		int			fc_bshift = TransactionIdToBIndex(fc_xid) * CLOG_BITS_PER_XACT;
		int			fc_slotno;
		char	   *fc_byteptr;

		fc_slotno = SimpleLruReadPage(XactCtl, fc_pageno, false, fc_xid);
		fc_byteptr = XactCtl->shared->page_buffer[fc_slotno] + fc_byteno;

		/* 将当前字节中尚未使用的位置置零 */
		*fc_byteptr &= (1 << fc_bshift) - 1;
		/* 将页面的其余部分置零 */
		MemSet(fc_byteptr + 1, 0, BLCKSZ - fc_byteno - 1);

		XactCtl->shared->page_dirty[fc_slotno] = true;
	}

	LWLockRelease(XactSLRULock);
}

/*
 * 执行检查点 --- 无论是在关闭期间还是动态执行
 */
void CheckPointCLOG(void)
{
	/*
	 * 将脏 CLOG 页面写入磁盘。这可能会导致同步请求
	 * 在 ProcessSyncRequests() 中排队，作为检查点的一部分。
	 */
	TRACE_POSTGRESQL_CLOG_CHECKPOINT_START(true);
	SimpleLruWriteAll(XactCtl, true);
	TRACE_POSTGRESQL_CLOG_CHECKPOINT_DONE(true);
}


/*
 * 确保 CLOG 有足够的空间来容纳新分配的 XID。
 *
 * 注意：此时持有 XidGenLock。我们希望它在大多数情况下
 * 都能非常快；即使不是那么快，实际的 I/O 也不需要发生，
 * 除非我们被迫写出一个脏 CLOG 或 XLOG 页，以便在共享内存中
 * 腾出空间。
 */
void ExtendCLOG(TransactionId fc_newestXact)
{
	int			fc_pageno;

	/*
	 * 除了页面的第一个 XID，没什么工作要做。但要注意：
	 * 刚刚环绕过后，页面零的第一个 XID 是
	 * FirstNormalTransactionId。
	 */
	if (TransactionIdToPgIndex(fc_newestXact) != 0 &&
		!TransactionIdEquals(fc_newestXact, FirstNormalTransactionId))
		return;

	fc_pageno = TransactionIdToPage(fc_newestXact);

	LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);

	/* 将页面置零并创建一个关于它的 XLOG 条目 */
	fc_ZeroCLOGPage(fc_pageno, true);

	LWLockRelease(XactSLRULock);
}


/*
 * 在持有传入事务 ID 的段之前，删除所有 CLOG 段
 *
 * 在删除任何 CLOG 数据之前，我们必须将 XLOG 刷新到磁盘，以确保
 * 最近发出的 FREEZE_PAGE 记录已经写入磁盘；否则
 * 崩溃和重启可能会留下某些未解冻的元组引用已删除的 CLOG 数据。
 * 我们选择发出一个特殊的 TRUNCATE XLOG 记录。
 * 从 XLOG 重放删除操作并不是至关重要的，因为文件
 * 也可以稍后被删除，但这样做可以防止长时间运行的热
 * 备用服务器获取不合理膨胀的 CLOG 目录。
 *
 * 由于 CLOG 段持有大量事务，因此实际删除段的机会相对较少，
 * 因此在确认有可删除段之前，最好不要进行 XLOG 刷新。
 */
void TruncateCLOG(TransactionId fc_oldestXact, Oid fc_oldestxid_datoid)
{
	int			fc_cutoffPage;

	/*
	 * 截止点是包含 oldestXact 的段的开始。我们
	 * 将包含 oldestXact 的 *页面* 传递给 SimpleLruTruncate。
	 */
	fc_cutoffPage = TransactionIdToPage(fc_oldestXact);

	/* 检查是否有任何可以删除的文件 */
	if (!SlruScanDirectory(XactCtl, SlruScanDirCbReportPresence, &fc_cutoffPage))
		return;					/* 没有什么可删除的 */

	/*
	 * 在截断 clog 之前提前 oldestClogXid，以便并发事务状态
	 * 查找可以确保它们不会尝试访问被截断的 clog。
	 *
	 * 只有在我们实际截断 clog 页面时才有必要这样做。
	 */
	AdvanceOldestClogXid(fc_oldestXact);

	/*
	 * 写入 XLOG 记录并将 XLOG 刷新到磁盘。我们记录下
	 * 正在保留信息的最旧 xid，以确保在崩溃时
	 * 它总是在 clog 截断之前，从而让备用在下一个检查点之前
	 * 了解新的有效 xid。
	 */
	fc_WriteTruncateXlogRec(fc_cutoffPage, fc_oldestXact, fc_oldestxid_datoid);

	/* 现在我们可以删除旧的 CLOG 段 */
	SimpleLruTruncate(XactCtl, fc_cutoffPage);
}


/*
 * 决定 CLOG 页面编号在截断目的上是否“更旧”。
 *
 * 我们需要在这里使用 TransactionIds 的比较，以便正确处理
 * 回绕 XID 算法。但是，TransactionIdPrecedes()
 * 对永久事务 ID 会变得奇怪。因此，将两个进行偏移，使得
 * xid1、xid2 和 xid2 + CLOG_XACTS_PER_PAGE - 1 都是正常的 XIDs；
 * 该偏移与页面 0 及页面 0 之前的页面相关。
 *
 * 包含 oldestXact-2^31 的页面是重要的边界情况。
 * 该页面中等于或跟随 oldestXact-2^31 的部分是可抛弃的，
 * 但在 oldestXact-2^31 之前的部分则不可抛弃。
 * 当 oldestXact-2^31 是页面和段的第一个 XID 时，
 * 整个页面和段都是可抛弃的，我们可以截断该段。
 * 识别该情况需要将 oldestXact，而不仅仅是包含 oldestXact 的页面，
 * 提供给该回调。好处将是罕见而微小，因此我们
 * 并不优化该边界情况。
 */
static bool fc_CLOGPagePrecedes(int fc_page1, int fc_page2)
{
	TransactionId fc_xid1;
	TransactionId fc_xid2;

	fc_xid1 = ((TransactionId) fc_page1) * CLOG_XACTS_PER_PAGE;
	fc_xid1 += FirstNormalTransactionId + 1;
	fc_xid2 = ((TransactionId) fc_page2) * CLOG_XACTS_PER_PAGE;
	fc_xid2 += FirstNormalTransactionId + 1;

	return (TransactionIdPrecedes(fc_xid1, fc_xid2) &&
			TransactionIdPrecedes(fc_xid1, fc_xid2 + CLOG_XACTS_PER_PAGE - 1));
}


/*
 * 写入 ZEROPAGE xlog 记录
 */
static void fc_WriteZeroPageXlogRec(int fc_pageno)
{
	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_pageno), sizeof(int));
	(void) XLogInsert(RM_CLOG_ID, CLOG_ZEROPAGE);
}

/*
 * 写入 TRUNCATE xlog 记录
 *
 * 我们必须在返回之前将 xlog 记录刷新到磁盘 --- 参见
 * TruncateCLOG() 中的说明。
 */
static void fc_WriteTruncateXlogRec(int fc_pageno, TransactionId fc_oldestXact, Oid fc_oldestXactDb)
{
	XLogRecPtr	fc_recptr;
	xl_clog_truncate fc_xlrec;

	fc_xlrec.pageno = fc_pageno;
	fc_xlrec.oldestXact = fc_oldestXact;
	fc_xlrec.oldestXactDb = fc_oldestXactDb;

	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_xlrec), sizeof(xl_clog_truncate));
	fc_recptr = XLogInsert(RM_CLOG_ID, CLOG_TRUNCATE);
	XLogFlush(fc_recptr);
}

/*
 * CLOG 资源管理器的例程
 */
void clog_redo(XLogReaderState *fc_record)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;

	/* 在 clog 记录中不使用备份块 */
	Assert(!XLogRecHasAnyBlockRefs(fc_record));

	if (fc_info == CLOG_ZEROPAGE)
	{
		int			fc_pageno;
		int			fc_slotno;

		memcpy(&fc_pageno, XLogRecGetData(fc_record), sizeof(int));

		LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);

		fc_slotno = fc_ZeroCLOGPage(fc_pageno, false);
		SimpleLruWritePage(XactCtl, fc_slotno);
		Assert(!XactCtl->shared->page_dirty[fc_slotno]);

		LWLockRelease(XactSLRULock);
	}
	else if (fc_info == CLOG_TRUNCATE)
	{
		xl_clog_truncate fc_xlrec;

		memcpy(&fc_xlrec, XLogRecGetData(fc_record), sizeof(xl_clog_truncate));

		AdvanceOldestClogXid(fc_xlrec.oldestXact);

		SimpleLruTruncate(XactCtl, fc_xlrec.pageno);
	}
	else
		elog(PANIC, "clog_redo: unknown op code %u", fc_info);
}

/*
 * sync.c的入口点，用于同步clog文件。
 */
int clogsyncfiletag(const FileTag *fc_ftag, char *fc_path)
{
	return SlruSyncFileTag(XactCtl, fc_ftag, fc_path);
}
